﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;

namespace ExpressionLanguage
{
    internal class ExpressionScanner
    {
        public ExpressionScanner(string scripts)
        {
            Script_ = scripts;
            CurrentIndex_ = 0;
        }

        public enum TToken
        {
            UNKNOWN= 0, // it must be zero
            ADD,
            SUB,
            DIV,
            MUL,

            EQ,
            NEQ,
            GT,
            GET,
            LT,
            LET,
            AND,
            OR,
            NOT,

            LEFT_SCOPE,
            RIGHT_SCOPE,

            LEFT_SQUARE,
            RIGHT_SQUARE,

            IF,
            ELIF,
            ELSE,

            NEW,
            POST,
            CTOR,

            VARIABLE,
            STRING,
            NUMBER,
            END,
            AMP,
            LEFT_BRACKET,
            RIGHT_BRACKET,
            COMMA,
            ASSIGN,
            COMMENT,
            EOF
        }

        public static string GetTokenName(TToken token)
        {
            switch (token)
            {
                case TToken.ADD: return "ADD";
                case TToken.SUB: return "SUB";
                case TToken.DIV: return "DIV";
                case TToken.MUL: return "MUL";
                case TToken.GT:  return ">";
                case TToken.GET: return ">=";
                case TToken.LT:  return "<";
                case TToken.LET: return "<=";
                case TToken.EQ:  return "==";
                case TToken.NEQ: return "!=";
                case TToken.VARIABLE: return "VAR";
                case TToken.STRING: return "STRING";
                case TToken.NUMBER: return "NUMBER";
                case TToken.END: return ";";
                case TToken.AMP: return "\"";
                case TToken.LEFT_BRACKET: return "(";
                case TToken.RIGHT_BRACKET: return ")";
                case TToken.LEFT_SCOPE: return "{";
                case TToken.RIGHT_SCOPE: return "}";
                case TToken.LEFT_SQUARE: return "[";
                case TToken.RIGHT_SQUARE: return "]";
                case TToken.IF: return "if";
                case TToken.ELIF: return "elif";
                case TToken.ELSE: return "else";
                case TToken.NEW: return "new";
                case TToken.CTOR: return "ctor";
                case TToken.POST: return "post";
                case TToken.AND: return "&&";
                case TToken.OR: return "||";
                case TToken.NOT: return "!";
                case TToken.COMMA: return ",";
                case TToken.UNKNOWN: return "UNKNOWN";
                case TToken.ASSIGN: return "=";
                case TToken.EOF: return "EOF";
                default:
                    return "I don't know!";
            }
        }

        public struct LexInfo
        {
            public TToken Token
            {
                get;
                set;
            }

            public int Index
            {
                get;
                set;
            }

            public int End
            {
                get;
                set;
            }

            public string Script
            {
                get;
                set;
            }

            public string Slice
            {
                get 
                { 
                    if (Index >= Script.Length) 
                        return "<eof>";
                    else
                        return Script.Substring(Index, End - Index);
                }
            }

            public bool AsNumber(out double number)
            {
                number = default(double);
                string strval = Script.Substring(Index, End - Index);
                try
                {
                    number = Convert.ToDouble(strval);
                }
                catch (FormatException)
                {
                    return false;
                }
                return true;
            }

            public bool AsString(out string str)
            {
                str = default(string);
                StringBuilder buffer = new StringBuilder();

                for (int i = Index + 1; i < End - 1; )
                {
                    char cha = Script[i];
                    if (cha != '\\')
                    {
                        buffer.Append(cha);
                        ++i;
                    }
                    else
                    {
                        if (i + 1 != End - 1)
                        {
                            char nextChar = Script[i + 1];
                            // control characters that we are known
                            if (nextChar == 'b' || nextChar == 'n' || nextChar == 't')
                            {
                                buffer.Append('\\');
                                buffer.Append(nextChar);
                            }
                            else
                            {
                                buffer.Append(nextChar);
                            }
                        }
                        else
                        {
                            return false;
                        }
                        i += 2;
                    }
                }
                str = buffer.ToString();
                return true;
            }


            public bool AsVariable(out string name)
            {
                name = Script.Substring(Index, End - Index);
                return true;
            }

        }

        private enum TNState
        {
            FD,// first digit
            I, // digits
            D  // dot
        }

        private TToken TokenizeNumber(string script, int position, out int startPosition, out int nextPosition)
        {
            int i;
            TNState states = TNState.FD;
            startPosition = position;
            nextPosition = position + 1;

            for (i = position; i < script.Length; ++i)
            {
                switch (states)
                {
                    case TNState.FD:
                        if( !Char.IsNumber(script[i]) )
                        {
                            return TToken.UNKNOWN;
                        }
                        states = TNState.I;
                        break;
                    case TNState.I:
                        if (script[i] == '.')
                        {
                            states = TNState.D;
                        }
                        else if (!Char.IsNumber(script[i]))
                        {
                            nextPosition = i;
                            goto finish;
                        }
                        break;
                    case TNState.D:
                        if (!Char.IsNumber(script[i]))
                        {
                            nextPosition = i;
                            goto finish;
                        }
                        break;
                    default:
                        break;
                }
            }

            if (script.Length == i)
                return TToken.UNKNOWN;
        finish:
            return TToken.NUMBER;
        }


        private enum TSS
        {
            I,
            S
        }
        // I will skip the escape character but not remove them, the escape character only effects this function to
        // search the end of the string constant. The script will not be modified .
        private TToken TokenizeString(string script, int position, out int startPosition, out int nextPosition)
        {
            int i;
            TSS state = TSS.I;
            startPosition = position;
            nextPosition = position + 1;
            for (i = position; i < script.Length; ++i)
            {
                switch (state)
                {
                    case TSS.I:
                        if (script[i] == '"')
                            state = TSS.S;
                        else
                            return TToken.UNKNOWN;
                        break;
                    case TSS.S:
                        if (script[i] == '"')
                        {
                            nextPosition = i + 1;
                            goto finish;
                        }
                        else if (script[i] == '\\')
                        {
                            ++i;
                        }
                        break;
                    default:
                        break;
                }
            }

            if (script.Length == i)
                return TToken.UNKNOWN;
        finish:
            return TToken.STRING;
        }

        private bool IsValidVariableSymbol(char cha)
        {
            return Char.IsLetter(cha) || Char.IsNumber(cha) || cha == '_' || cha == '.';
        }

        private TToken TokenizeVariable(string script, int position, out int startPosition, out int nextPosition)
        {
            int i;
            startPosition = position;
            nextPosition = position + 1;
            for (i = position+1; i < script.Length; ++i)
            {
                if (!IsValidVariableSymbol(script[i]))
                    break;
            }
            nextPosition = i;
            return TToken.VARIABLE;
        }

        private void SkipWhitespace(string script, int position, out int nextPosition)
        {
            while ( position < script.Length && Char.IsWhiteSpace(script[position]) )
            {
                ++position;
            }
            nextPosition = position;
        }

        private void SkipComments(string script,int position,out int nextPosition)
        {
            while(position<script.Length && script[position] !='\n')
            {
                ++position;
            }
            if (position < script.Length)
                nextPosition = position + 1;
            else
                nextPosition = position;
        }

        private struct Shelter
        {
            public Shelter( string str )
            {
                String_ = str;
            }

            public char Get( int index )
            {
                if (index >= String_.Length)
                    return '\0';
                else
                    return String_[index];
            }

            private string String_;
        }

        private TToken Tokenize(string script, int position, out int startPosition, out int nextPosition)
        {
            // Check if we need to exit the loop
            if (script.Length <= position)
            {
                startPosition = position;
                nextPosition = position + 1;
                return TToken.EOF;
            }

            // Skip all the white space and make it go
            SkipWhitespace(script, position, out position);
            startPosition = position;
            nextPosition = position + 1;
            Shelter shelter = new Shelter(script);
            char cha = shelter.Get(position);

            switch (cha)
            {
                case '0':case '1':case '2':case '3':case '4':
                case '5':case '6':case '7':case '8':case '9':
                    // Parsing number
                    return TokenizeNumber(script, position, out startPosition, out nextPosition);
                case '+':
                    return TToken.ADD;
                case '-':
                    return TToken.SUB;
                case '*':
                    return TToken.MUL;
                case '/':
                    return TToken.DIV;
                case '=':
                    if( shelter.Get(position+1) == '=' )
                    {
                        nextPosition = position + 2;
                        return TToken.EQ;
                    }
                    else
                    {
                        return TToken.ASSIGN;
                    }
                case '>':
                    if( shelter.Get(position+1) == '=' )
                    {
                        nextPosition = position + 2;
                        return TToken.GET;
                    }
                    else
                    {
                        return TToken.GT;
                    }
                case '<':
                    if( shelter.Get(position+1) == '=' )
                    {
                        nextPosition = position + 2;
                        return TToken.LET;
                    }
                    else
                    {
                        return TToken.LT;
                    }
                case '!':
                    if( shelter.Get(position+1) == '=' )
                    {
                        nextPosition = position + 2;
                        return TToken.NEQ;
                    }
                    else
                    {
                        return TToken.NOT;
                    }
                case '&':
                    if( shelter.Get(position+1) == '&' )
                    {
                        nextPosition = position + 2;
                        return TToken.AND;
                    }
                    else
                    {
                        return TToken.UNKNOWN;
                    }
                case '|':
                    if( shelter.Get(position+1) == '|' )
                    {
                        nextPosition = position + 2;
                        return TToken.OR;
                    }
                    else
                    {
                        return TToken.UNKNOWN;
                    }
                case 'i':
                    if (shelter.Get(position + 1) == 'f' && !IsValidVariableSymbol(shelter.Get(position+2)) )
                    {
                        nextPosition = position + 2;
                        return TToken.IF;
                    }
                    else
                        goto default;
                case 'e':
                    if (shelter.Get(position + 1) == 'l')
                    {
                        if (shelter.Get(position + 2) == 'i' && shelter.Get(position + 3) == 'f' 
                            && !IsValidVariableSymbol(shelter.Get(position + 4)))
                        {
                            nextPosition = position + 4;
                            return TToken.ELIF;
                        }
                        else if (shelter.Get(position + 2) == 's' && shelter.Get(position + 3) == 'e' 
                            && !IsValidVariableSymbol(shelter.Get(position + 4)))
                        {
                            nextPosition = position + 4;
                            return TToken.ELSE;
                        }
                        else
                            goto default;
                    }
                    else
                        goto default;
                case 'c':
                    if (shelter.Get(position + 1) == 't' && shelter.Get(position + 2) == 'o' &&
                       shelter.Get(position + 3) == 'r' && !IsValidVariableSymbol(shelter.Get(position + 4)))
                    {
                        nextPosition = position + 4;
                        return TToken.CTOR;
                    }
                    else
                        goto default;
                case 'n':
                    if (shelter.Get(position + 1) == 'e' && shelter.Get(position + 2) == 'w' && !IsValidVariableSymbol(shelter.Get(position + 3)))
                    {
                        nextPosition = position + 3;
                        return TToken.NEW;
                    }
                    else
                        goto default;
                case 'p':
                    if (shelter.Get(position + 1) == 'o' && shelter.Get(position + 2) == 's' &&
                       shelter.Get(position + 3) == 't' && !IsValidVariableSymbol(shelter.Get(position + 4)))
                    {
                        nextPosition = position + 4;
                        return TToken.POST;
                    }
                    else
                        goto default;
                case ';':
                    return TToken.END;
                case ',':
                    return TToken.COMMA;
                case '(':
                    return TToken.LEFT_BRACKET;
                case ')':
                    return TToken.RIGHT_BRACKET;
                case '{':
                    return TToken.LEFT_SCOPE;
                case '}':
                    return TToken.RIGHT_SCOPE;
                case '[':
                    return TToken.LEFT_SQUARE;
                case ']':
                    return TToken.RIGHT_SQUARE;
                case '\"':
                    return TokenizeString(script, position, out startPosition, out nextPosition);
                case '_':
                case '@':
                    return TokenizeVariable(script, position, out startPosition, out nextPosition);
                case ':':
                    SkipComments(script, position, out nextPosition);
                    return TToken.COMMENT;
                default:
                    if (Char.IsLetter(cha))
                        return TokenizeVariable(script, position, out startPosition, out nextPosition);
                    else
                        return TToken.UNKNOWN;
            }
        }

        public LexInfo Next()
        {
            LexInfo info = new LexInfo();
            int startPosition;
            int endPosition;
            while (true)
            {
                info.Token = Tokenize(Script_, CurrentIndex_, out startPosition, out endPosition);
                if (info.Token == ExpressionScanner.TToken.COMMENT)
                {
                    CurrentIndex_ = endPosition;
                    startPosition = endPosition;
                }
                else
                {
                    break;
                }
            }
            info.Index = startPosition;
            info.End = endPosition;
            info.Script = Script_;
            CurrentIndex_ = endPosition;
            return info;
        }

        public LexInfo Peek()
        {
            LexInfo info = new LexInfo();
            int startPosition;
            int endPosition;
            while (true)
            {
                info.Token = Tokenize(Script_, CurrentIndex_, out startPosition, out endPosition);
                if (info.Token == ExpressionScanner.TToken.COMMENT)
                {
                    CurrentIndex_ = endPosition;
                    startPosition = endPosition;
                }
                else
                {
                    break;
                }
            }
            info.Index = startPosition;
            info.End = endPosition;
            info.Script = Script_;
            // We still need to adjust the index since the space may be skipped 
            if (startPosition > CurrentIndex_)
                CurrentIndex_ = startPosition;
            return info;
        }

        public bool Move(LexInfo info)
        {
            if (info.Index != CurrentIndex_)
                return false;
            CurrentIndex_ = info.End;
            return true;
        }

        private int CurrentIndex_;
        private string Script_;
    }
}
